<?php

namespace Aspic\Model;

use Aspic as A;
use Aspic\Exception;
use Aspic\Preset\AspicErrorException;

/**
 * Row with possibility to save data and get if from persistant access (database, files, ..)
 */
class DynamicRow extends STRow {

	const PERSIST_NO = 0;

	const PERSIST_NOT_RECURSIVE = 1;

	const PERSIST_RECURSIVE = 2;

	/**
	 * @var bool Specify if joins should be retrieve when trying to get a var with name equal to
	 * a foreign key property name
	 * @see Aspic\Model\DataModel
	 */
	protected $_autoFetchJoins;

	protected $_persistState;

	public function __construct(DataModel $dataModel, $returnNullIfEmpty = true) {
		parent::__construct($dataModel, $returnNullIfEmpty);

		$this->_autoFetchJoins = true;
		$this->_persist = self::PERSIST_NO;
	}

	public function __destruct() {
		if ($this->_hasBeenModified) {
			if ($this->_persistState != self::PERSIST_NO) {
				if ($this->_persistState == self::PERSIST_NOT_RECURSIVE) {
					$recursive = false;
				}
				elseif ($this->_persistState == self::PERSIST_RECURSIVE) {
					$recursive = true;
				}
				else {
					throw new AspicErrorException('Unknown persist state');
				}

				$this->save($recursive);
			}
		}
	}

	public function setDataModel(DataModel $dataModel) {
		if (!$dataModel->hasCrudModel()) {
			throw new A\Exception('The given DataModel does not have a CrudModel associate with it. This is neccessary whe using DynamicRow');
		}

		parent::setDataModel($dataModel);
	}

	public function setAutoFetchJoin($val) {
		$this->_autoFetchJoins = (bool) $val;
	}

	public function &__get($var) {
		if ($this->_autoFetchJoins == true) {
			// Checking if we have to fetch data from db for a join - so we look for a join object name which cannot be a column
			if ($this->hasDataModel() AND ! $this->getDataModel()->colExists($var)) {
				$fk = $this->getDataModel()->getForeignKeyByPropertyName($var);

				// Is there a foreign key corresponding ?
				if ($fk) {
					// $var is a fk propertyName
					$col = $this->getDataModel()->getCol($fk['colName']);
					$colValue = &$this->get($col['name']);

					// Is the "propertyName column" value empty AND the model associated with a CRUD model AND foreign key isset ?
					// Should we fetch object from DB or not ?
					if (
						$fk['dataModel']->hasCrudModel() AND
						isset($colValue) AND // To retrieve the foreign key data, foreign key value must be set
						( !isset($this->{$fk['propertyName']}) OR ( // Maybe we have already set a value to it
						// But maybe the the foreign key value has change and isn't anymore the same as object
						$this->{$fk['propertyName']}->getPrimaryKeyValue() != $colValue
						)
						)
					) {
						// So we fetch data from db to fill it
						$joinCrudModel = $fk['dataModel']->getCrudModel();
						$row = $joinCrudModel->readRow($colValue);

						$this->{$fk['propertyName']} = $row;

						return $row;
					}
					// When trying to get a propertyName of a FK that is not already defined, we return an empty object
					else {
						// This call __set and set the fk value to null because object has no PK
						$this->{$fk['propertyName']} = $fk['dataModel']->getRowInstance();
					}
				}
				else {
					$ofk = $this->getDataModel()->getOppositeForeignKeyByPropertyName($var);

					// Is there an opposite foreign key corresponding ?
					if ($ofk) {
						$col = $this->getDataModel()->getCol($ofk['colName']);
						$colValue = &$this->get($col['name']);

						// Is the "propertyName column" value empty AND the model associated with a CRUD model AND foreign key isset ?
						if (
							$ofk['dataModel']->hasCrudModel() AND ! isset($this->{$ofk['propertyName']}) AND
							isset($colValue)
						) {
							// So we fetch data from db to fill it
							$joinCrudModel = $ofk['dataModel']->getCrudModel();
							$row = $joinCrudModel->fetchRows($ofk['colName'], $this->getPrimaryKeyValue());

							$this->{$ofk['propertyName']} = $row;

							return $row;
						}
					}
				}
			}
		}

		return parent::__get($var);
	}

	public function __set($var, $value) {
		// When unserializing, DataModel could not be set
		// If column does exist, treatement go to parent method
		if ($this->hasDataModel() AND ! $this->getDataModel()->colExists($var)) {
			$fk = $this->getDataModel()->getForeignKeyByPropertyName($var);

			if ($fk) {
				if ($value instanceof Row AND $value->getDataModel() == $fk->getDataModel()) {
					$obj = $value;
					$col = $this->getDataModel()->getCol($fk['colName']);

					// Object primary key is set, so it should already exist
					if ($obj->isPrimaryKeySet()) {
						$fkKeyValue = $obj->getPrimaryKeyValue();
					}
					// The object has just been created and do not yet exist in the DB
					elseif ($obj->getDataModel()->isPrimaryKeyAI()) {
						$fkKeyValue = null;
					}
					// The primary key is not set and it is not an auto_increment PK, this could not be saved in DB
					else {
						throw new Exception('Trying to set foreign key with an object that havn\'t primary key an which isn\'t set auto_increment');
					}

					// Setting foreign key value to object primary key
					$this->set($fk['colName'], $fkKeyValue);
				}
				else {
					throw new Exception('Cannot set anything else then the corresponding Row with matching DataModel');
				}
			}
			// If user is updating the foreign key value, we unset the corresponding (propertyName) object
			elseif ($this->getDataModel()->foreignKeyExists($var)) {
				$fk = $this->getDataModel()->getForeignKey($var);

				unset($this->{$fk['propertyName']});
			}
		}

		return parent::__set($var, $value);
	}

	/**
	 * Check if object is already in DB or not (When possible)
	 */
	public function isNewObject() {
		$pk = $this->getDataModel()->getPrimaryKey();

		if ($pk['autoIncrement'] == false) {
			throw new A\Exception('Primary key is not set to auto increment => Cannot determine if row should be insert or update.');
		}
		else {
			if (!is_null($this->getPrimaryKeyValue())) {
				return false;
			}
			else {
				return true;
			}
		}
	}

	/**
	 * Make row persistant
	 * This method choose between updateRow and insertRow from the existance in the data of the primaryKeyValue.
	 * It suppose then that the primaryKey is an autoincrement and is not set that is because it does not exists yet
	 * and would be added by the DB.
	 * @param bool $makePersistant If true, method will call $this->persist()
	 */
	public function save($recursive = true) {

		if ($this->isNewObject() == false) {
			$this->update($recursive);
		}
		else {
			$id = $this->insert($recursive);

			// Setting primary key on the object when it's auto_increment
			if ($this->getDataModel()->getPrimaryKey()['autoIncrement']) {
				$this->{$this->getDataModel()->getPrimaryKeyName()} = $id;
			}
		}

		$this->_hasBeenModified = false;
	}

	public function persist($recursive = true) {
		if ($recursive) {
			$persistState = self::PERSIST_RECURSIVE;
		}
		else {
			$persistState = self::PERSIST_NOT_RECURSIVE;
		}

		$this->_persistState = $persistState;
	}

	/**
	 * When setting just created Row as foreign key (in associated
	 * "propertyName" property) and when foreign primary key is set to
	 * auto_increment, the main object does not know yet the foreign key
	 * value.
	 *
	 * This is because it will be determined at the insertion in
	 * the database. The purpose of this method is to synchronize
	 * foreign key value, to the primary key of the foreign row (so the
	 * row has to be saved in database before calling this method.
	 *
	 * This method has to be called when using recurise saving, after
	 * child objects have been saved, and before saving current object.
	 * If this is not called, current object foreign key with new objects,
	 * will not be linked (foreign key is set to null when row without
	 * primary key is set to fk "propertyName")
	 *
	 * No more util (a priori), we synchronize PK and FK in insert() method
	 *
	 * @see save
	 * @throws Exception
	 */
//	public function resolveForeignKeysIntegrity() {
//		$fks = $this->getDataModel()->getForeignKeys();
//
//		foreach ($fks as $fk) {
//			$fkValue = $this->{$fk['colName']};
//			$fkObj = $this->{$fk['propertyName']};
//
//			if ($fkObj) {
//				// Object primary key has been set when saving object but not yet synshronized with fkValue
//				if ($fkObj->isPrimaryKeySet() AND $fkValue == null) {
//					$this->{$fk['colName']} = $fkObj->getPrimaryKeyValue();
//				}
//				elseif ($fkObj->{$fk['colName']} != $fkValue) {
//					throw new Exception('Foreign key value and object\'s id does not match. This configuration should NEVER occurs (aspic code problem)');
//				}
//			}
//		}
//	}

	public function read($primaryKeyValue, $persist = true) {
		$data = $this->getDataModel()->getCrudModel()->readRow($primaryKeyValue);

		if (!is_null($data)) {
			$this->setVars($data->getVars());

			if ($persist) {
				$this->persist();
			}

			return true;
		}
		else {
			return false;
		}
	}

	public function delete($recurvise = true) {
		if ($recurvise) {
			return $this->getDataModel()->getCrudModel()->deleteRow($this->getPrimaryKeyValue(), null, true);
		}
		else {
			return $this->getDataModel()->getCrudModel()->deleteRow($this->getPrimaryKeyValue());
		}
	}

	/**
	 * @param boolean $recurvise Update recursively (only for foreign key not opposite fk)
	 * @todo Move recursive code to AbstractTable
	 */
	public function update($recurvise = true) {
		if ($recurvise) {
			foreach ($this->getDataModel()->getForeignKeys() as $fk) {
				$fkObj = $this->{$fk['propertyName']};

				// If object is set for foreign key
				if ($fkObj) {
					$fkObj->save(true);
				}
			}
		}

//		$this->resolveForeignKeysIntegrity();

		return $this->getDataModel()->getCrudModel()->updateRow($this, $recurvise);
	}

	/**
	 *
	 * @param type $recurvise
	 * @return type
	 * @todo Move recursive code to AbstractTable
	 */
	public function insert($recurvise = true) {
		if ($recurvise) {
			foreach ($this->getDataModel()->getForeignKeys() as $fk) {
				$fkObj = $this->{$fk['propertyName']};

				// If object is set for foreign key
				if ($fkObj) {
					$fkObj->save(true);

					// Synchronizing object primary key with parent foreign key
					$this->$fk['name'] = $fkObj->getPrimaryKeyValue();
				}
			}
		}

//		$this->resolveForeignKeysIntegrity();

		return $this->getDataModel()->getCrudModel()->addRow($this, $recurvise);
	}

}

?>